#!/usr/bin/python
#-*-coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

u"""
:name: tools_testing
:author: Guillaume Baty <guillaume.baty@andheo.fr>
:license: Copyright INRIA pyLot
"""

__all__ = [u'HandlerPart']

import os, sys
import doctest
from pyLot.core import (check_type, walk_files, Utils, Path, ENCODING, get_data,
                        hardbook, find_program, FileWriter, Handler)
from pyLot.application import APPLICATION
from _pylot.defaultconf import install_translator


from sphinx_docgen import generate_rst_class_doc, SphinxDocGenConf

class HandlerPart (Handler):
  u"""
  This class provides methods to test python files.
  """
  def __init__ (self, plugin_path=None, plugin_id=None):
    Handler.__init__(self)
    self.addOptions(get_data(__name__, u'conf.xml'), main=True)
    self.process = APPLICATION.getProcessHandler()
    self.io = self.process.io

    self._pylint_conf = self.findFile(u'pylint.conf')
    self._tests_results = {}

  def _setverbose(self, verbose):
    self.opt[u'doctests'][u'verbose'] = verbose
  verbose = property(fget=lambda self:self.opt[u'doctests'](u'verbose'),
                     fset=_setverbose)

  def _setwarning(self, warning):
    self.opt[u'doctests'][u'warning'] = warning
  warning = property(fget=lambda self:self.opt[u'doctests'](u'warning'),
                     fset=_setwarning)

  def _translate(self):
    self.alias = self.tr_(u'Code Testing')
    self.tooltip = self.tr_(u'A Plugin providing code testing methods')

  def pyLintFile(self, path, report=False, fmt=u'text'):
    u"""
    Checks statically python file "path" using pylint.
    pylot convention is used. See pylint.conf file for further information.
    
    :param report: display a summary (pylint option: --reports)
    :param fmt: change outpout format (pylint option: --output-format, -f)
    """
    check_type(u'path', path, [unicode, Path])
    path = Path(path)
    if report is True:
      report = u'yes'
    else:
      report = u'no'
    # report = u'yes' if report is True else u'no'
    return self.process.runCmd(u'pylint',
      [u'--rcfile="%s"' % self._pylint_conf,
       u'--reports=%s' % report,
       u'--output-format=%s' % fmt,
       u"%s" % path])

  def doctestFiles(self, path, pattern=u'*.rst', encoding=u'utf-8', globs={}):
    u"""
    Launches doctest file matching pattern in "path" directory.
    If "path" is a file and matches pattern, tests "path"
    Files must be raw text files (reStructuredText, doctest, etc.)
    
    :param globs: global variable dict
    """
    check_type(u'path', path, [unicode, Path, list, tuple, set])
    check_type(u'pattern', pattern, unicode)
    check_type(u'encoding', encoding, unicode)

    test_results = {}
    for doctestfile in walk_files(path, pattern) :

      self.io.write(u'*' * 80)
      self.io.write(u'\n%s\n' % doctestfile)
      self.io.write(u'*' * 80)
      self.io.write(u'\n')
      results = self.doctestFile(doctestfile, encoding, globs)
      test_results[u'file: %s' % doctestfile.relpath()] = results

    self._tests_results.update(test_results)

  def doctestFile(self, path, encoding=u'utf-8', globs={}):
    u"""
    Launches doctests in text file "path".
    File must be a raw text file (reStructuredText, doctest, etc.)
    
    :param globs: global variable dict
    """
    #test_results[u'file: %s' % doctestfile.relpath()]
    results = doctest.testfile(
        Path(path).abspath(),
        module_relative=False,
        raise_on_error=(not self.warning),
        verbose=self.verbose,
        globs=globs,
        encoding=encoding
        )
    # call translate method to install gettext again 
    # because doctest replaces gettext method "_" ?!?
    self.translate()
    return results

  def doctestModules (self, path):
    """
    test doctests in a python module
    """
    check_type(u'path', path, [unicode, Path, list, tuple, set])

    test_results = {}

    for pyfile in Utils.walkFiles(path, u'*.py') :
      module = Path(u'pylot').relpathto(pyfile).replaceext(u'').replace(os.sep, u'.')
      self.io.write(u'*' * 80 + u'\n')
      self.io.write(unicode(module) + u'\n')
      mod = __import__(module, fromlist=[u'xyz'])
      test_results[u'mod: %s' % module] = doctest.testmod(
        mod, raise_on_error=(not self.warning), verbose=self.verbose)
      self.io.write(u'*' * 80 + u'\n')
      install_translator(self.getCurrentLocale())

    self._tests_results.update(test_results)

  def checkProgramDependencies (self, program_list):
    for program in program_list :
      path = find_program(program)
      if isinstance(program, (set, tuple, list)) :
        msg = u' or '.join(program)
      else :
        msg = program
      txt = self.tr_(u'  -- Looking for "%s"') % msg
      txt = txt.ljust(40)
      if path is None :
        self.io.writeStderr(txt)
        self.io.writeStderr(self.tr_(u' - NOT FOUND\n'), style=u'warn')
        if not self.warning :
          sys.exit(127)
      else :
        self.io.writeStdout(txt)
        self.io.writeStdout(self.tr_(u' - found (%s)\n') % path, style=u'info')

  def checkModuleDependencies (self, module_list):
    for module in module_list :
      txt = self.tr_(u'  -- Looking for "%s"') % module
      txt = txt.ljust(40)
      try :
        mod = __import__(module)
      except ImportError :
        self.io.writeStderr(txt)
        self.io.writeStderr(self.tr_(u' - NOT FOUND\n'), style=u'warn')
      else :
        self.io.writeStdout(txt)
        self.io.writeStdout(self.tr_(u' - found (%s)\n') % mod, style=u'info')

  def displayResults(self):
    u"""
    Displays doctests results
    """
    TOT_PASSED = 0
    TOT_FAILED = 0
    group = self._tests_results
    if group :
      passed = 0
      failed = 0
      self.io.write(u'passed | failed ||  test\n')
      self.io.write(u'%s\n' % (u'-' * 80))
      for test_id, test in group.items() :
        stat = u'%6d | %6d ||' % (test[1], test[0])
        passed += test[1]
        failed += test[0]
        self.io.write(u'%s  %s\n' % (stat, test_id))
      self.io.write(u'%s\n' % (u'-' * 80))
      self.io.write(u'%6d | %6d || TOTAL\n' % (passed, failed))
      TOT_PASSED += passed
      TOT_FAILED += failed

  def example(self, n):
    u"""
    method for testing purpose only
    
    >>> handler = HandlerPart().init()
    >>> handler.example(2)
    2

    :return: n
    """
    return n

  def writeSphinxDoc(self, class_path, output, config=SphinxDocGenConf()):
    u"""
    Generates a reference rst file for class_path.
    class_path is full python path, for example pylot.core.Path.
    output is the path where you want to save generated rst file,
    for example u'docs/classPath.rst'.
    
    .. note::

      For pylot classes, you should use automatic generation method
      :meth:`~tools_testing.HandlerPart.actionAutoWritePypyLotReference`
    """
    self.io.write(self.tr_(u'Generate sphinx file %s\n') % output)
    rst_file = Utils.writefile(output, security=u'force')
    output_text = generate_rst_class_doc(class_path, config)
    rst_file.write(output_text)
    rst_file.close()

  def extractPyObjects(self, package, module_list):

    modules = {}
    classes = {}
    functions = {}
    attrs = {}

    package_mod = __import__(package, fromlist=module_list)

    for mod in module_list :
      modules[mod] = getattr(package_mod, mod)
      classes[mod] = {}
      functions[mod] = {}
      attrs[mod] = {}
      for class_name in dir(getattr(package_mod, mod)) :
        if not class_name.startswith('_') :
          obj = getattr(getattr(package_mod, mod), class_name)
          if str(obj).startswith('<class') :
            classes[mod][str(class_name)] = obj
          elif str(obj).startswith('<func') :
            functions[mod][str(class_name)] = obj
          else : #it's not a class but it may be an instance 
            if str(type(obj)).startswith("<class '_pylot"):
              attrs[mod][str(class_name)] = obj

    py_objects = dict(modules=modules, classes=classes,
                     functions=functions, attrs=attrs)
    return package_mod, py_objects

  def writeReference(self, package, udesc, umod, py_objects,
                     root_dir=None, title=None):

    modules = py_objects['modules']
    classes = py_objects['classes']
    functions = py_objects['functions']
    attrs = py_objects['attrs']

    desc = udesc.encode(ENCODING)
    mod = umod.encode(ENCODING)
    if root_dir is None :
      root_dir = udesc

    init = Path(udesc, u'desc_%s' % mod, u'__init__.py')
    if not init.parent.exists():
      init.parent.makedirs()
    init.touch()

    init = Path(udesc.parent, u'__init__.py')
    if not init.exists():
      init.touch()
    if desc not in sys.path :
      sys.path.append(desc)

    init = Path(udesc, u'__init__.py')
    if not init.exists():
      init.touch()

    desc_module = __import__('desc_%s' % mod, fromlist=classes[mod].keys())

    for class_ in classes[mod].keys() :
      uclass_ = class_.decode(ENCODING)

      class_path = '%s.%s.%s' % (package, mod, class_)
      output_text = Path(udesc, u'autoref_%s.rst' % uclass_)
      input_data = Path(udesc, u'desc_%s' % umod, u'%s.py' % uclass_)

      if not input_data.exists() :
        in_data = u"# -*- coding: utf-8 -*\n\n"
        in_data += u"short_desc = u'The %s class provides ...'\n" % class_
        in_data += u"long_desc = u''\n"
        in_data += u"authors = [\n  u'Guillaume Baty <guillaume.baty@andheo.fr>',\n  u'Pierre Puiseux <pierre.puiseux@univ-pau.fr>'\n]\n"
        in_data += u"summary_adds = u''\n"
        in_data += u"ref_adds = u''\n"

        self.io.write(self.tr_(u'Write initial data file %s\n') % input_data)

        in_file = Utils.writefile(input_data)
        in_file.write(in_data)
        in_file.close()

      config = SphinxDocGenConf()
      try :
        desc_config = getattr(desc_module, class_)
      except AttributeError :
        self.io.writeStderr(self.tr_(u'Loading "%s" fails !\n') % input_data)
      else :
        for attr_name in ('short_desc', 'long_desc', 'authors',
                          'summary_adds', 'ref_adds', 'force_refs'):
          try :
            attr = getattr(desc_config, attr_name)
          except AttributeError :
            pass
          else :
            setattr(config, attr_name, attr)

      self.writeSphinxDoc(class_path, output_text, config)
    sys.path.remove(desc)

    index = Path(root_dir, u'index.rst')
    if index.exists():
      f = None
      msg = self.tr_(u'\nYou can now update %s with these toctree in:\n\n') % index
      self.io.write(msg, style=u'warn')
      writers = None #default writer
    else :
      self.io.write(self.tr_(u'Write default index %s\n') % index)
      f = Utils.writefile(index)
      writers = [FileWriter(f)]

    if title :
      line = u'=' * len(title)
      self.io.write(u'%s\n%s\n%s\n' % (line, title, line), writers=writers)

    self.io.write(u'\n.. toctree::\n  :maxdepth: 1\n\n', writers=writers)
    class_keys = classes[mod].keys()
    class_keys.sort()
    for class_ in class_keys :
      path = root_dir.relpathto(udesc)
      self.io.write(u'  %s/autoref_%s.rst\n' % (path, class_), writers=writers)
    self.io.write(u'\n', writers=writers)

    if f : f.close()

  def actionAutoWritePypyLotReference(self):
    u"""
    This action generates a class reference file for each class in pylot.
    Generation is automatic but can be customized using py file configuration
    in desc_MODULE directories, where MODULE is 'core', 'qtgui', etc.
    
    Rst files are saved in :
      - :file:`pylot/doc/user_manual/reference/MODULE/ClassName.rst`
    Configuration in
      - :file:`pylot/doc/user_manual/reference/MODULE/desc_MODULE/ClassName.py`
    
    """
    package = u'pyLot'
    module_list = ['core', 'interfaces', 'qtgui', 'dataconverter', 'physics']
    docdir = Path(u'doctests/user_manual/reference/')
    self.autoWriteReference(package, module_list, docdir)

  def autoWriteReference(self, package, module_list, docdir):
    u"""
    This action generates a class reference file for each class in pylot.
    Generation is automatic but can be customized using py file configuration
    in desc_MODULE directories, where MODULE is 'core', 'qtgui', etc.
    
    Rst files are saved in :
      - :file:`pylot/doc/user_manual/reference/MODULE/ClassName.rst`
    Configuration in
      - :file:`pylot/doc/user_manual/reference/MODULE/desc_MODULE/ClassName.py`
    
    """
    package_obj, py_objects = self.extractPyObjects(package, module_list)
    for mod in py_objects['classes']:
      umod = mod.decode(ENCODING)
      udesc = Path(docdir, umod)
      self.writeReference(package, udesc, umod, py_objects,
                           root_dir=udesc.parent)

  def actionCheckPypyLot(self):
    self.doctestFiles(hardbook(u'pylotsrc', u'pyLotTests', u'doctests'),
                         u'*.rst')
    self.doctestModules(u'pylot/_pylot')
    self.displayResults()

  def actionCheckPypyLotDeps(self):
    self.checkProgramDependencies([u'cmake', u'g++', u'doxygen', u'pdflatex',
                                   u'sphinx-build', u'linguist'])
    self.checkModuleDependencies([u'PyQt4', u'subprocess', u're'])

  def _writeSettings(self, settings, common):
    for book_id in self.opt :
      settings.beginGroup(u'tools_testing_%s' % book_id)
      settings.setBook(self.opt[book_id])
      settings.endGroup()

  def _readSettings(self, settings, common):
    for book_id in self.opt :
      settings.beginGroup(u'tools_testing_%s' % book_id)
      book = settings.getBook(default_book=self.opt[book_id])
      self.opt[book_id].userUpgrade(book)
      settings.endGroup()


if __name__ == '__main__':
  pass
